/*--------------------------------------------------------------------------------
 * Adaptive Intelligence - Windows Forms Controls Class Library
 * Version 2.00
 * 
 * Advanced Combo Box Class
 *      Extension of Windows Combo box with autosizing, text search, read-only, 
 * and item tagging capabilities.
 *------------------------------------------------------------------------------*/

/*--------------------------------------------------------------------------------
 * System Imports
 *------------------------------------------------------------------------------*/
using System;
using System.Text;
using System.ComponentModel;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;
using Adaptive.Foundation.Collections;

namespace Adaptive.Foundation.Windows.Forms
{
    /// <summary>
    /// Extension of Windows Combo box with autosizing, text search, read-only, 
    /// and item tagging capabilities.
    /// </summary>
    [ToolboxBitmap(typeof(EmbeddedResourceFinder), "Adaptive.Foundation.Windows.Forms.AdvancedComboBox.bmp")]
    [Browsable(true), ToolboxItem(true), DesignerCategory("UserControl")]
    public partial class AdvancedComboBox : ComboBox
    {
        /*--------------------------------------------------------------------------------
         * Public Event Definitions
         *------------------------------------------------------------------------------*/
        #region Public Event Definitions
        #endregion

        /*--------------------------------------------------------------------------------
         * Private Constants Declarations
         *------------------------------------------------------------------------------*/
        #region Private Constants Declarations
        #endregion

        /*--------------------------------------------------------------------------------
         * Private Member Declarations
         *------------------------------------------------------------------------------*/
        #region Private Member Declarations
        /// <summary>
        /// Read-Only flag.
        /// </summary>
        private bool _readOnly = false;
        /// <summary>
        /// Keystroke memory.
        /// </summary>
        private string _memory = string.Empty;
        /// <summary>
        /// Keystroke tick timer.
        /// </summary>
        private long _ticks = 0;
        /// <summary>
        /// Tag list.
        /// </summary>
        private StringCollection _tags;
        /// <summary>
        /// Object attachments.
        /// </summary>
        private List<object> _attachments;
        #endregion

        /*--------------------------------------------------------------------------------
         * Constructor / Dispose / Destructor Methods
         *------------------------------------------------------------------------------*/
        #region Constructor / Dispose / Destructor Methods
        /// <summary>
        /// Initializes a new instance of the class.
        /// </summary>
        /// <remarks>
        /// This is the default constructor.
        /// </remarks>
        public AdvancedComboBox()
            : base()
        {
            DoubleBuffered = true;
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

            InitializeComponent();

            _tags = new StringCollection();
            _attachments = new List<object>();

        }
        /// <summary>
        /// Deallocates internally allocated objects.
        /// </summary>
        /// <param name="disposing">
        /// A value indicating whether the object is being disposed.
        /// </param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                    components.Dispose();

                if (_tags != null)
                    _tags.Dispose();

                if (_attachments != null)
                    _attachments.Clear();

            }

            _memory = null;
            _tags = null;
            _attachments = null;
            components = null;
            base.Dispose(disposing);
        }
        #endregion

        /*--------------------------------------------------------------------------------
         * Public Properties
         *------------------------------------------------------------------------------*/
        #region Public Properties
        /// <summary>
        /// Gets or sets a value indicating whether the control can respond to user interaction.
        /// </summary>
        /// <value>
        /// Returns <b>true</b> if the control can respond to user interaction; 
        /// otherwise returns <b>false</b>.
        /// </value>
        /// <remarks>
        /// Setting this property to either <b>true</b> or <b>false</b>sets the 
        /// <see cref="ReadOnly"/> property to <b>false</b>.
        /// </remarks>
        public new bool Enabled
        {
            get
            {
                return base.Enabled;
            }
            set
            {
                _readOnly = false;
                SetStyle(ControlStyles.UserPaint, false);
                base.Enabled = value;
                Invalidate();
            }
        }
        /// <summary>
        /// Gets or sets a value indicating whether the control can respond to the user
        /// interface, without appearing disabled.
        /// </summary>
        /// <value>
        /// Returns <b>true</b> if the control can respond to user interaction; 
        /// otherwise returns <b>false</b>.
        /// </value>
        /// <remarks>
        /// The purpose of this property is to allow the caller to effectively disable
        /// the combo box control, while retaining a normal appearance.
        /// </remarks>
        [Browsable(true), DefaultValue(false), Localizable(true),
        Category("Appearance"),
        Description("Sets the combo box to normal or read-only mode.")]
        public bool ReadOnly
        {
            get
            {
                return _readOnly;
            }
            set
            {
                base.Enabled = !value;
                SetStyle(ControlStyles.UserPaint, value);
                _readOnly = value;
                Invalidate();
            }
        }
        /// <summary>
        /// Gets the tag value for the currently selected item, if one is selected.
        /// </summary>
        /// <value>
        /// Returns a string value containing the related string tag that has been
        /// assigned to a member item, if an item is selected.  If an item is not 
        /// selected, returns "0"; if no tag item exists, returns a blank string.
        /// </value>
        [Browsable(false)]
        public string SelectedTag
        {
            get
            {
                if (SelectedIndex == -1)
                {
                    return "0";
                }
                else
                {
                    return _tags[SelectedIndex];
                }
            }
        }
        /// <summary>
        /// Gets the tag object assigned to the currently selected item.
        /// </summary>
        /// <value>
        /// Returns an <b>object</b> that was assigned to the currently selected item.
        /// If no item is selected, or no object was assigned to the currently selected
        /// item, returns <b>null</b>.
        /// </value>
        [Browsable(false)]
        public object SelectedObjectReference
        {
            get
            {
                object returnValue = null;

                if (SelectedIndex > -1)
                    returnValue = _attachments[SelectedIndex];

                return returnValue;
            }
        }
        /// <summary>
        /// Returns the width of the drop-down box.
        /// </summary>
        /// <value>
        /// Returns the pixel width of the drop-down box.
        /// </value>
        /// <remarks>
        /// This property is automatic on this version of the combo box.  This property 
        /// is no longer settable, as the drop-down window will automatically size itself
        /// to its contents.
        /// </remarks>
        public new int DropDownWidth
        {
            get
            {
                return CalculateWidth();
            }
        }
        #endregion

        /*--------------------------------------------------------------------------------
         * Protected Methods / Event Methods
         *------------------------------------------------------------------------------*/
        #region Protected Methods / Event Methods
        /// <summary>
        /// Paints the control and raises the <see cref="OnPaint"/> event.
        /// </summary>
        /// <remarks>
        /// This method is only activated when the control is in read-only mode.
        /// </remarks>
        /// <param name="e">
        /// A <see cref="PaintEventArgs"/> event arguments object containing the 
        /// <see cref="Graphics"/> object used to paint the control.
        /// </param>
        protected override void OnPaint(PaintEventArgs e)
        {
            System.Drawing.SolidBrush backBrush = null;
            System.Drawing.Pen pen = null;
            int x = 0;
            int y = 0;
            int width = 0;
            int height = 0;

            x = e.ClipRectangle.X;
            y = e.ClipRectangle.Y;
            width = (e.ClipRectangle.Width - 1);
            height = (e.ClipRectangle.Height - 1);

            //Draw the background rectangle.
            backBrush = new SolidBrush(BackColor);
            e.Graphics.FillRectangle(backBrush, e.ClipRectangle);
            backBrush.Dispose();

            //Draw the border lines.
            pen = SystemPens.ControlDark;
            e.Graphics.DrawLine(pen, x, y, x, y + y + height);
            e.Graphics.DrawLine(pen, x, y, x + width, y);

            pen = SystemPens.ControlLightLight;
            e.Graphics.DrawLine(pen, x + 1, y + height, x + width, y + height);
            e.Graphics.DrawLine(pen, x + width, y + height, x + width, y);

            pen = SystemPens.ControlDarkDark;
            e.Graphics.DrawRectangle(pen,
                new Rectangle(x + 1, y + 1, x + width - 2, y + height - 2));

            pen = SystemPens.Control;
            e.Graphics.DrawLine(pen, x + 1, y + height - 1, x + width - 1, y + height - 1);
            e.Graphics.DrawLine(pen, x + width - 1, y + height - 1, x + width - 1, y + 1);

            //Draw the text.
            backBrush = new SolidBrush(ForeColor);
            e.Graphics.DrawString(Text, Font, backBrush, 2, 4);
            backBrush.Dispose();
            backBrush = null;
            pen = null;
        }
        /// <summary>
        /// Auto-sizes the drop down box and raises the <see cref="ComboBox.DropDown"/> event.
        /// </summary>
        /// <param name="e">
        /// A <see cref="System.EventArgs"/> event arguments object.
        /// </param>
        protected override void OnDropDown(EventArgs e)
        {
            base.DropDownWidth = CalculateWidth();
            base.OnDropDown(e);
        }
        /// <summary>
        /// For each keystroke, searches the combo box for a matching item.
        /// </summary>
        /// <param name="e">
        /// A <see cref="KeyPressEventArgs"/> event arguments object containing the keystroke
        /// information.
        /// </param>
        protected override void OnKeyPress(KeyPressEventArgs e)
        {
            SetMemory(e.KeyChar.ToString());
            SelectedIndex = FindString(_memory);
            e.Handled = true;
        }
        #endregion

        /*--------------------------------------------------------------------------------
         * Public Methods / Functions
         *------------------------------------------------------------------------------*/
        #region Public Methods / Functions
        /// <summary>
        /// Synchronizes the local tag and object references collections to 
        /// the count of items in the Items collection.
        /// </summary>
        /// <remarks>
        /// The internal Tags collection and Objects collection are set to the
        /// number of items in the Items collection, but each tag is a blank string and object
        /// reference is set to null.
        /// </remarks>
        public void SyncItems()
        {
            // Synchronizes the local tag and object references collections to
            // the count of items in the Items collection.
            _attachments.Clear();
            _tags.CreateEmptyContent(Items.Count);
        }
        /// <summary>
        /// Sets the currently selected item to the item with the specified tag value.
        /// </summary>
        /// <param name="tag">The tag item of the object to be selected.</param>
        public void SelectTag(string tag)
        {
            SelectedIndex = _tags.IndexOf(tag);
        }
        /// <summary>
        /// Gets the value of a tagged item at the specified index.
        /// </summary>
        /// <param name="index">The item index to get the tag for.</param>
        /// <returns>Returns the tag value for the specified item.</returns>
        public new string Tag(int index)
        {
            return _tags[index];
        }
        /// <summary>
        /// Sets the tag value for an item at the specified index.
        /// </summary>
        /// <param name="index">The index of the item being tagged.</param>
        /// <param name="value">The tag to apply to the specified item.</param>
        public new void Tag(int index, string value)
        {
            if (_tags.Count - 1 < index)
                _tags.Add(value);
            else
                _tags[index] = value;
        }
        /// <summary>
        /// Gets the object data for the item at the specified index.
        /// </summary>
        /// <param name="index">The index of the item being referenced.</param>
        /// <returns>Returns the object data assigned to the specified item, if 
        /// present.  Otherwise, returns <b>null</b>.</returns>
        public object ObjectReference(int index)
        {
            return _attachments[index];
        }
        /// <summary>
        /// Sets the object data for the item at the specified index.
        /// </summary>
        /// <param name="index">The index of the object being assigned object data.</param>
        /// <param name="itemToAttach">The object/data to assign to the specified item.</param>
        public void ObjectReference(int index, object itemToAttach)
        {
            _attachments.Add(itemToAttach);
        }
        #endregion

        /*--------------------------------------------------------------------------------
         * Private Methods / Functions
         *------------------------------------------------------------------------------*/
        #region Private Methods / Functions
        /// <summary>
        /// Calculates the width for the drop-down text box.
        /// </summary>
        /// <returns>
        /// The new width for the drop-down, in pixels.
        /// </returns>
        private int CalculateWidth()
        {
            Graphics g = null;				//Graphics object used for measurement.
            int returnValue = 0;			//Return value.
            int count = 0;					//Iteration counter.
            int length = 0;					//Iteration length.
            string text = string.Empty;		//Text to measure.
            SizeF size;						//Size measurement.

            //Create the graphics object.
            g = CreateGraphics();

            //Measure each string and store the value of the greatest width.
            length = Items.Count;
            for (count = 0; count < length; count++)
            {
                text = Items[count].ToString();
                size = g.MeasureString(text, Font);
                if (size.Width > returnValue)
                    returnValue = (int)size.Width + 1;
            }

            //Clear and return.
            g.Dispose();
            g = null;
            text = null;

            if (returnValue < Width)
                returnValue = Width;
            return returnValue;
        }
        /// <summary>
        /// Sets the keystroke search memory.
        /// </summary>
        /// <param name="newChar">The new character pressed by the user.</param>
        private void SetMemory(string newChar)
        {
            TimeSpan ts;

            if (_memory.Length == 0)
            {
                _memory = newChar;
                _ticks = DateTime.Now.Ticks;
            }
            else
            {
                //Capture the elapsed time and reset the count.
                ts = new TimeSpan(DateTime.Now.Ticks - _ticks);
                _ticks = DateTime.Now.Ticks;

                //If the elapsed time > 6/10 of a second, reset the memory container,
                //else append the keystroke.
                if (ts.TotalMilliseconds < 600)
                {
                    _memory += newChar;
                }
                else
                {
                    _memory = newChar;
                }
            }
        }
        #endregion

    }
}